Atraskite pažangų vaizdo apdorojimą naršyklėje. Išmokite tiesiogiai pasiekti ir manipuliuoti neapdorotais VideoFrame plokštumų duomenimis su WebCodecs API, kuriant unikalius efektus ir analizę.
WebCodecs VideoFrame plokštumų prieiga: išsamus neapdorotų vaizdo duomenų manipuliavimo vadovas
Ilgus metus didelio našumo vaizdo apdorojimas naršyklėje atrodė tolima svajonė. Kūrėjai dažnai buvo apriboti <video> elemento ir 2D Canvas API galimybėmis, kurios, nors ir galingos, sukeldavo našumo problemas ir ribotą prieigą prie pagrindinių neapdorotų vaizdo duomenų. WebCodecs API atsiradimas iš esmės pakeitė šią situaciją, suteikdamas žemo lygio prieigą prie naršyklėje integruotų medijos kodekų. Viena iš revoliucingiausių jos funkcijų yra galimybė tiesiogiai pasiekti ir manipuliuoti atskirų vaizdo kadrų neapdorotais duomenimis per VideoFrame objektą.
Šis straipsnis yra išsamus vadovas kūrėjams, norintiems pereiti nuo paprasto vaizdo įrašų atkūrimo. Jame nagrinėsime VideoFrame plokštumų prieigos subtilybes, paaiškinsime tokias sąvokas kaip spalvų erdvės ir atminties išdėstymas bei pateiksime praktinių pavyzdžių, kurie leis jums kurti naujos kartos naršyklės vaizdo programas – nuo realaus laiko filtrų iki sudėtingų kompiuterinės regos užduočių.
Būtinosios sąlygos
Norėdami gauti kuo daugiau naudos iš šio vadovo, turėtumėte gerai išmanyti:
- Šiuolaikinį JavaScript: Įskaitant asinchroninį programavimą (
async/await, Promises). - Pagrindines vaizdo sąvokas: Naudinga susipažinti su tokiais terminais kaip kadrai, skiriamoji geba ir kodekai.
- Naršyklės API: Patirtis su API, tokiomis kaip Canvas 2D ar WebGL, bus naudinga, bet nėra griežtai privaloma.
Vaizdo kadrų, spalvų erdvių ir plokštumų supratimas
Prieš pradedant nagrinėti API, pirmiausia turime susikurti tvirtą mentalinį modelį, kaip iš tikrųjų atrodo vaizdo kadro duomenys. Skaitmeninis vaizdo įrašas yra nejudančių vaizdų arba kadrų seka. Kiekvienas kadras yra pikselių tinklelis, o kiekvienas pikselis turi spalvą. Kaip ta spalva yra saugoma, apibrėžia spalvų erdvė ir pikselių formatas.
RGBA: gimtoji interneto kalba
Dauguma interneto kūrėjų yra susipažinę su RGBA spalvų modeliu. Kiekvienas pikselis yra pavaizduojamas keturiais komponentais: raudona (Red), žalia (Green), mėlyna (Blue) ir alfa (Alpha, skaidrumas). Duomenys paprastai atmintyje saugomi persipynusiu būdu (interleaved), o tai reiškia, kad vieno pikselio R, G, B ir A vertės saugomos viena po kitos:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Šiame modelyje visas vaizdas saugomas viename ištisiniame atminties bloke. Galime tai įsivaizduoti kaip vieną duomenų „plokštumą“.
YUV: vaizdo glaudinimo kalba
Tačiau vaizdo kodekai retai dirba tiesiogiai su RGBA. Jie teikia pirmenybę YUV (arba tiksliau, Y'CbCr) spalvų erdvėms. Šis modelis vaizdo informaciją padalija į:
- Y (Luma): Šviesumo arba pustonių (grayscale) informacija. Žmogaus akis yra jautriausia šviesumo pokyčiams.
- U (Cb) ir V (Cr): Spalvingumo (chrominance) arba spalvų skirtumo informacija. Žmogaus akis yra mažiau jautri spalvų detalumui nei šviesumo detalumui.
Šis atskyrimas yra raktas į efektyvų glaudinimą. Sumažindami U ir V komponentų skiriamąją gebą – technika, vadinama spalvingumo diskretizavimu (chroma subsampling) – galime žymiai sumažinti failo dydį su minimaliu pastebimu kokybės praradimu. Tai lemia planarinius pikselių formatus, kur Y, U ir V komponentai saugomi atskiruose atminties blokuose, arba „plokštumose“.
Įprastas formatas yra I420 (YUV 4:2:0 tipas), kur kiekvienam 2x2 pikselių blokui yra keturi Y pavyzdžiai, bet tik po vieną U ir V pavyzdį. Tai reiškia, kad U ir V plokštumų plotis ir aukštis yra perpus mažesni nei Y plokštumos.
Suprasti šį skirtumą yra kritiškai svarbu, nes WebCodecs suteikia jums tiesioginę prieigą prie šių plokštumų, lygiai taip, kaip jas pateikia dekoderis.
VideoFrame objektas: jūsų vartai į pikselių duomenis
Centrinė šios dėlionės dalis yra VideoFrame objektas. Jis atspindi vieną vaizdo kadrą ir jame yra ne tik pikselių duomenys, bet ir svarbūs metaduomenys.
Pagrindinės VideoFrame savybės
format: Eilutė, nurodanti pikselių formatą (pvz., 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Visi kadro matmenys, kaip jie saugomi atmintyje, įskaitant bet kokį kodeko reikalaujamą užpildymą (padding).displayWidth/displayHeight: Matmenys, kurie turėtų būti naudojami kadrui atvaizduoti.timestamp: Kadro pateikimo laiko žyma mikrosekundėmis.duration: Kadro trukmė mikrosekundėmis.
Magiškasis metodas: copyTo()
Pagrindinis metodas neapdorotiems pikselių duomenims pasiekti yra videoFrame.copyTo(destination, options). Šis asinchroninis metodas kopijuoja kadro plokštumų duomenis į jūsų pateiktą buferį.
destination:ArrayBufferarba tipizuotas masyvas (pvz.,Uint8Array), pakankamai didelis duomenims sutalpinti.options: Objektas, nurodantis, kurias plokštumas kopijuoti ir jų atminties išdėstymą. Jei praleistas, kopijuoja visas plokštumas į vieną ištisinį buferį.
Metodas grąžina Promise, kuris išsisprendžia su PlaneLayout objektų masyvu, po vieną kiekvienai kadro plokštumai. Kiekviename PlaneLayout objekte yra du svarbūs informacijos elementai:
offset: Baitų poslinkis, nuo kurio prasideda šios plokštumos duomenys paskirties buferyje.stride: Baitų skaičius tarp vienos pikselių eilutės pradžios ir kitos eilutės pradžios toje plokštumoje.
Kritiškai svarbi sąvoka: žingsnis (Stride) vs. plotis (Width)
Tai yra vienas iš dažniausių painiavos šaltinių kūrėjams, kurie yra naujokai žemo lygio grafikos programavime. Negalima daryti prielaidos, kad kiekviena pikselių duomenų eilutė yra glaudžiai supakuota viena po kitos.
- Plotis (Width) yra pikselių skaičius vaizdo eilutėje.
- Žingsnis (Stride) (taip pat vadinamas pitch arba line step) yra baitų skaičius atmintyje nuo vienos eilutės pradžios iki kitos eilutės pradžios.
Dažnai stride bus didesnis nei plotis * baitų_per_pikselį. Taip yra todėl, kad atmintis dažnai papildoma (padded), kad atitiktų aparatinės įrangos ribas (pvz., 32 ar 64 baitų ribas) greitesniam apdorojimui CPU ar GPU. Visada turite naudoti žingsnį (stride), kad apskaičiuotumėte pikselio atminties adresą konkrečioje eilutėje.
Ignoruojant žingsnį, vaizdai bus iškreipti arba deformuoti, o duomenų prieiga bus neteisinga.
Praktinis pavyzdys 1: pustonių (grayscale) plokštumos prieiga ir atvaizdavimas
Pradėkime nuo paprasto, bet galingo pavyzdžio. Dauguma vaizdo įrašų internete yra koduojami YUV formatu, pavyzdžiui, I420. 'Y' plokštuma iš esmės yra pilnas pustonių (grayscale) vaizdo atvaizdas. Galime išgauti tik šią plokštumą ir ją atvaizduoti drobėje (canvas).
async function displayGrayscale(videoFrame) {
// Darome prielaidą, kad videoFrame yra YUV formato, pavyzdžiui, 'I420' arba 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Šiam pavyzdžiui reikalingas YUV 4:2:0 planarinis formatas.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y plokštuma visada yra pirma.
// Sukuriame buferį, kuriame bus laikomi tik Y plokštumos duomenys.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Nukopijuojame Y plokštumą į mūsų buferį.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Dabar yPlaneData yra neapdoroti pustonių (grayscale) pikseliai.
// Mums reikia jį atvaizduoti. Sukursime RGBA buferį drobei (canvas).
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Iteruojame per drobės pikselius ir užpildome juos duomenimis iš Y plokštumos.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Svarbu: Naudokite žingsnį (stride), kad rastumėte teisingą šaltinio indeksą!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Apskaičiuojame paskirties indeksą RGBA ImageData buferyje.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Raudona
imageData.data[rgbaIndex + 1] = luma; // Žalia
imageData.data[rgbaIndex + 2] = luma; // Mėlyna
imageData.data[rgbaIndex + 3] = 255; // Alfa (skaidrumas)
}
}
ctx.putImageData(imageData, 0, 0);
// KRITIŠKAI SVARBU: Visada uždarykite VideoFrame, kad atlaisvintumėte jo atmintį.
videoFrame.close();
}
Šis pavyzdys pabrėžia kelis pagrindinius veiksmus: teisingo plokštumos išdėstymo nustatymą, paskirties buferio paskirstymą, copyTo naudojimą duomenims išgauti ir teisingą iteravimą per duomenis naudojant stride, kad būtų sukurtas naujas vaizdas.
Praktinis pavyzdys 2: manipuliavimas vietoje (Sepia filtras)
Dabar atlikime tiesioginę duomenų manipuliaciją. Sepia filtras yra klasikinis efektas, kurį lengva įgyvendinti. Šiam pavyzdžiui lengviau dirbti su RGBA kadru, kurį galite gauti iš drobės (canvas) ar WebGL konteksto.
async function applySepiaFilter(videoFrame) {
// Šis pavyzdys daro prielaidą, kad įvesties kadras yra 'RGBA' arba 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Sepia filtro pavyzdžiui reikalingas RGBA kadras.');
videoFrame.close();
return null;
}
// Paskiriame buferį pikselių duomenims laikyti.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA yra viena plokštuma
// Dabar manipuliuojame duomenimis buferyje.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 baitai vienam pikseliui (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) lieka nepakeista.
}
}
// Sukuriame *naują* VideoFrame su pakeistais duomenimis.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Nepamirškite uždaryti pradinio kadro!
videoFrame.close();
return newFrame;
}
Tai demonstruoja pilną skaitymo-modifikavimo-rašymo ciklą: nukopijuojame duomenis, iteruojame per juos naudojant žingsnį (stride), pritaikome matematinę transformaciją kiekvienam pikseliui ir sukuriame naują VideoFrame su gautais duomenimis. Šis naujas kadras gali būti atvaizduotas drobėje, išsiųstas į VideoEncoder arba perduotas kitam apdorojimo etapui.
Našumas svarbu: JavaScript vs. WebAssembly (WASM)
Iteravimas per milijonus pikselių kiekviename kadre (1080p kadras turi virš 2 milijonų pikselių, arba 8 milijonus duomenų taškų RGBA formate) naudojant JavaScript gali būti lėtas. Nors šiuolaikiniai JS varikliai yra neįtikėtinai greiti, realaus laiko didelės raiškos vaizdo (HD, 4K) apdorojimui šis metodas gali lengvai perkrauti pagrindinę giją (main thread), sukeldamas trūkinėjančią vartotojo patirtį.
Čia WebAssembly (WASM) tampa esminiu įrankiu. WASM leidžia jums paleisti kodą, parašytą tokiomis kalbomis kaip C++, Rust ar Go, beveik natūraliu greičiu naršyklės viduje. Vaizdo apdorojimo darbo eiga tampa tokia:
- JavaScript'e: Naudokite
videoFrame.copyTo(), kad gautumėte neapdorotus pikselių duomenis įArrayBuffer. - Perdavimas į WASM: Perduokite nuorodą į šį buferį į savo sukompiliuotą WASM modulį. Tai labai greita operacija, nes ji neapima duomenų kopijavimo.
- WASM'e (C++/Rust): Vykdykite savo aukštai optimizuotus vaizdo apdorojimo algoritmus tiesiogiai atminties buferyje. Tai yra kelis kartus greičiau nei JavaScript ciklas.
- Grįžimas į JavaScript: Kai WASM baigia darbą, valdymas grįžta į JavaScript. Tada galite naudoti modifikuotą buferį, kad sukurtumėte naują
VideoFrame.
Bet kokiai rimtai, realaus laiko vaizdo manipuliavimo programai, tokiai kaip virtualūs fonai, objektų aptikimas ar sudėtingi filtrai, WebAssembly panaudojimas yra ne tik galimybė, bet ir būtinybė.
Skirtingų pikselių formatų tvarkymas (pvz., I420, NV12)
Nors RGBA yra paprastas, dažniausiai iš VideoDecoder gausite kadrus planariniuose YUV formatuose. Pažiūrėkime, kaip tvarkyti visiškai planarinį formatą, pavyzdžiui, I420.
VideoFrame I420 formatu turės tris išdėstymo deskriptorius savo layout masyve:
layout[0]: Y plokštuma (šviesumas). Matmenys yracodedWidthxcodedHeight.layout[1]: U plokštuma (spalvingumas). Matmenys yracodedWidth/2xcodedHeight/2.layout[2]: V plokštuma (spalvingumas). Matmenys yracodedWidth/2xcodedHeight/2.
Štai kaip nukopijuotumėte visas tris plokštumas į vieną buferį:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts yra 3 PlaneLayout objektų masyvas
console.log('Y plokštumos išdėstymas:', layouts[0]); // { offset: 0, stride: ... }
console.log('U plokštumos išdėstymas:', layouts[1]); // { offset: ..., stride: ... }
console.log('V plokštumos išdėstymas:', layouts[2]); // { offset: ..., stride: ... }
// Dabar galite pasiekti kiekvieną plokštumą `allPlanesData` buferyje
// naudodami jos konkretų poslinkį ir žingsnį.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Atkreipkite dėmesį, kad spalvingumo (chroma) matmenys yra perpus mažesni!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Pasiektos Y plokštumos dydis:', yPlaneView.byteLength);
console.log('Pasiektos U plokštumos dydis:', uPlaneView.byteLength);
videoFrame.close();
}
Kitas įprastas formatas yra NV12, kuris yra pusiau planarinis. Jis turi dvi plokštumas: vieną Y, o antrą plokštumą, kurioje U ir V vertės yra persipynusios (pvz., [U1, V1, U2, V2, ...]). WebCodecs API tai tvarko skaidriai; VideoFrame NV12 formatu tiesiog turės du išdėstymus savo layout masyve.
Iššūkiai ir geriausios praktikos
Darbas tokiame žemame lygyje yra galingas, bet su juo ateina ir atsakomybės.
Atminties valdymas yra svarbiausias
VideoFrame laiko didelį kiekį atminties, kuri dažnai valdoma už JavaScript šiukšlių surinkėjo (garbage collector) krūvos (heap). Jei aiškiai neatlaisvinsite šios atminties, sukelsite atminties nutekėjimą, kuris gali priversti naršyklės skirtuką užlūžti.
Visada, visada kvieskite videoFrame.close(), kai baigiate darbą su kadru.
Asinchroninė prigimtis
Visa duomenų prieiga yra asinchroninė. Jūsų programos architektūra turi tinkamai tvarkyti Promises ir async/await srautą, kad būtų išvengta lenktynių sąlygų (race conditions) ir užtikrintas sklandus apdorojimo procesas.
Naršyklių suderinamumas
WebCodecs yra moderni API. Nors palaikoma visose pagrindinėse naršyklėse, visada patikrinkite jos prieinamumą ir būkite informuoti apie bet kokias specifines gamintojo įgyvendinimo detales ar apribojimus. Prieš bandydami naudoti API, naudokite funkcijos aptikimą.
Išvada: naujas horizontas interneto vaizdo įrašams
Galimybė tiesiogiai pasiekti ir manipuliuoti neapdorotais VideoFrame plokštumų duomenimis per WebCodecs API yra paradigmų kaita interneto medijos programoms. Ji pašalina <video> elemento „juodąją dėžę“ ir suteikia kūrėjams detalų valdymą, anksčiau skirtą tik vietinėms (native) programoms.
Suprasdami vaizdo atminties išdėstymo pagrindus – plokštumas, žingsnį (stride) ir spalvų formatus – ir pasinaudodami WebAssembly galia našumui kritiškoms operacijoms, dabar galite kurti neįtikėtinai sudėtingus vaizdo apdorojimo įrankius tiesiogiai naršyklėje. Nuo realaus laiko spalvų korekcijos ir individualių vizualinių efektų iki kliento pusės mašininio mokymosi ir vaizdo analizės – galimybės yra didžiulės. Didelio našumo, žemo lygio vaizdo įrašų era internete iš tiesų prasidėjo.